/**
 * Copyright 2009-2019 the original author or authors.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.ibatis.parsing;

import org.apache.ibatis.builder.BuilderException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * @author Clinton Begin
 * @author Kazuki Shimizu
 */
public class XPathParser {
	/**
	 * Document 对象代表整个 XML 文件
	 */
	private final Document document;
	private boolean validation;
	/**
	 * 实体类解析器的接口
	 */
	private EntityResolver entityResolver;
	private Properties variables;
	private XPath xpath;
	
	public XPathParser(String xml) {
		commonConstructor(false, null, null);
		this.document = createDocument(new InputSource(new StringReader(xml)));
	}
	
	private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
		this.validation = validation;
		this.entityResolver = entityResolver;
		this.variables = variables;
		// 创建 XPathFactory 用于创建 XPath 对象
		XPathFactory factory = XPathFactory.newInstance();
		this.xpath = factory.newXPath();
	}
	
	/**
	 * @param inputSource
	 * @return
	 */
	private Document createDocument(InputSource inputSource) {
		// important: this must only be called AFTER common constructor
		try {
			// 创建一个 DocumentBuilderFactory 对象
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			// 设置 工厂对象的各种属性
			factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
			factory.setValidating(validation);
			
			factory.setNamespaceAware(false);
			factory.setIgnoringComments(true); // 忽略注释
			factory.setIgnoringElementContentWhitespace(false); // 忽略空格
			factory.setCoalescing(false);
			factory.setExpandEntityReferences(true);
			// 调用工厂的 newDocumentBuilder() 方法生成 DocumentBuilder 对象
			DocumentBuilder builder = factory.newDocumentBuilder();
			// builder 对象的属性设置：设置实体类的解析器
			builder.setEntityResolver(entityResolver);
			// 设置错误处理器
			builder.setErrorHandler(new ErrorHandler() {
				@Override
				public void warning(SAXParseException exception) throws SAXException {
					// NOP
				}
				
				@Override
				public void error(SAXParseException exception) throws SAXException {
					throw exception;
				}
				
				@Override
				public void fatalError(SAXParseException exception) throws SAXException {
					throw exception;
				}
			});
			// builder 调用 parse() 方法，将 配置文件输入流转化的 inputSource 生成 Document 对象返回。
			// JDK中的 DocumentBuilderImpl 实现类，用来解析 输入流
			return builder.parse(inputSource);
		} catch (Exception e) {
			throw new BuilderException("Error creating document instance.  Cause: " + e, e);
		}
	}
	
	public XPathParser(Reader reader) {
		commonConstructor(false, null, null);
		this.document = createDocument(new InputSource(reader));
	}
	
	public XPathParser(InputStream inputStream) {
		commonConstructor(false, null, null);
		this.document = createDocument(new InputSource(inputStream));
	}
	
	public XPathParser(Document document) {
		commonConstructor(false, null, null);
		this.document = document;
	}
	
	public XPathParser(String xml, boolean validation) {
		commonConstructor(validation, null, null);
		this.document = createDocument(new InputSource(new StringReader(xml)));
	}
	
	public XPathParser(Reader reader, boolean validation) {
		commonConstructor(validation, null, null);
		this.document = createDocument(new InputSource(reader));
	}
	
	public XPathParser(InputStream inputStream, boolean validation) {
		commonConstructor(validation, null, null);
		this.document = createDocument(new InputSource(inputStream));
	}
	
	public XPathParser(Document document, boolean validation) {
		commonConstructor(validation, null, null);
		this.document = document;
	}
	
	public XPathParser(String xml, boolean validation, Properties variables) {
		commonConstructor(validation, variables, null);
		this.document = createDocument(new InputSource(new StringReader(xml)));
	}
	
	public XPathParser(Reader reader, boolean validation, Properties variables) {
		commonConstructor(validation, variables, null);
		this.document = createDocument(new InputSource(reader));
	}
	
	public XPathParser(InputStream inputStream, boolean validation, Properties variables) {
		commonConstructor(validation, variables, null);
		this.document = createDocument(new InputSource(inputStream));
	}
	
	public XPathParser(Document document, boolean validation, Properties variables) {
		commonConstructor(validation, variables, null);
		this.document = document;
	}
	
	public XPathParser(String xml, boolean validation, Properties variables, EntityResolver entityResolver) {
		commonConstructor(validation, variables, entityResolver);
		this.document = createDocument(new InputSource(new StringReader(xml)));
	}
	
	public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
		commonConstructor(validation, variables, entityResolver);
		this.document = createDocument(new InputSource(reader));
	}
	
	/**
	 * XPathParser 构造器
	 * @param inputStream    配置文件转化的输入流
	 * @param validation     是否验证
	 * @param variables      属性变量
	 * @param entityResolver 实体类解析器
	 */
	public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
		// 调用 commonConstructor 方法
		commonConstructor(validation, variables, entityResolver);
		// 首先将 配置文件输入流 通过 new InputSource(inputStream) 构造成 InputSource 对象
		// 然后再通过方法 createDocument() 创建生成 Document 实例对象
		this.document = createDocument(new InputSource(inputStream));
	}
	
	public XPathParser(Document document, boolean validation, Properties variables, EntityResolver entityResolver) {
		commonConstructor(validation, variables, entityResolver);
		this.document = document;
	}
	
	public void setVariables(Properties variables) {
		this.variables = variables;
	}
	
	public String evalString(String expression) {
		return evalString(document, expression);
	}
	
	public String evalString(Object root, String expression) {
		String result = (String) evaluate(expression, root, XPathConstants.STRING);
		result = PropertyParser.parse(result, variables);
		return result;
	}
	
	private Object evaluate(String expression, Object root, QName returnType) {
		try {
			return xpath.evaluate(expression, root, returnType);
		} catch (Exception e) {
			throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
		}
	}
	
	public Boolean evalBoolean(String expression) {
		return evalBoolean(document, expression);
	}
	
	public Boolean evalBoolean(Object root, String expression) {
		return (Boolean) evaluate(expression, root, XPathConstants.BOOLEAN);
	}
	
	public Short evalShort(String expression) {
		return evalShort(document, expression);
	}
	
	public Short evalShort(Object root, String expression) {
		return Short.valueOf(evalString(root, expression));
	}
	
	public Integer evalInteger(String expression) {
		return evalInteger(document, expression);
	}
	
	public Integer evalInteger(Object root, String expression) {
		return Integer.valueOf(evalString(root, expression));
	}
	
	public Long evalLong(String expression) {
		return evalLong(document, expression);
	}
	
	public Long evalLong(Object root, String expression) {
		return Long.valueOf(evalString(root, expression));
	}
	
	public Float evalFloat(String expression) {
		return evalFloat(document, expression);
	}
	
	public Float evalFloat(Object root, String expression) {
		return Float.valueOf(evalString(root, expression));
	}
	
	public Double evalDouble(String expression) {
		return evalDouble(document, expression);
	}
	
	public Double evalDouble(Object root, String expression) {
		return (Double) evaluate(expression, root, XPathConstants.NUMBER);
	}
	
	public List<XNode> evalNodes(String expression) {
		return evalNodes(document, expression);
	}
	
	public List<XNode> evalNodes(Object root, String expression) {
		List<XNode> xnodes = new ArrayList<>();
		NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
		for (int i = 0; i < nodes.getLength(); i++) {
			xnodes.add(new XNode(this, nodes.item(i), variables));
		}
		return xnodes;
	}
	
	public XNode evalNode(String expression) {
		return evalNode(document, expression);
	}
	
	public XNode evalNode(Object root, String expression) {
		Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
		if (node == null) {
			return null;
		}
		return new XNode(this, node, variables);
	}
	
}
